package demo.service;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

/**
 * 类似数据库的行锁
 * 
 * @author hanjy
 *
 */
public class RowLock<T> {

    public RowLock(StringRedisTemplate stringRedisTemplate, String keyPattern) {
        if (stringRedisTemplate == null || keyPattern == null || keyPattern.isEmpty()) {
            throw new IllegalArgumentException("argument is null");
        }
        this.stringRedisTemplate = stringRedisTemplate;
        this.valueOperations = stringRedisTemplate.opsForValue();
        this.keyPattern = keyPattern;
    }

    private String keyPattern;

    private static Logger logger = LoggerFactory.getLogger(RowLock.class);

    private StringRedisTemplate stringRedisTemplate;

    private ValueOperations<String, String> valueOperations;

    private ConcurrentHashMap<T, Object> map = new ConcurrentHashMap<>(256);

    /**
     * 等待锁
     * 
     * @param id
     * @param timeout
     */
    public boolean waitingLock(T id, int timeout) {
        if (id == null) {
            return false;
        }
        waitLocalLock(id, timeout / 2 + 1);
        return waitRedisLock(id, timeout / 2 + 1);
    }

    /**
     * 释放锁
     * 
     * @param id
     * @param timeout
     */
    public void releaseLock(T id) {
        if (id == null) {
            return;
        }
        releaseRedisLock(id);
        releaseLocalLock(id);
    }

    /**
     * 等待REDIS锁
     * 
     * @param id
     * @param timeout
     */
    public boolean waitRedisLock(T id, int timeout) {
        String key = String.format(keyPattern, id);
        long t1 = System.currentTimeMillis();
        boolean result = false;
        try {
            result = valueOperations.setIfAbsent(key, "");
            while (!result) {
                safeSleep(timeout / 5);
                logger.info("setIfAbsent {} failed,try again", key);
                result = valueOperations.setIfAbsent(key, "");
                if (System.currentTimeMillis() - t1 > timeout) {
                    logger.warn("setIfAbsent {} timeout,result:{}", key, result);
                    break;
                }
            }
        } catch (Exception e) {
            logger.error("redis setIfAbsent {} failed", key, e);
        }
        if (result) {
            logger.info("setIfAbsent {} success", key);
            try {
                stringRedisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);
            } catch (Exception e) {
                logger.error("redis expire {} failed", key, e);
            }
        }
        return result;
    }

    /**
     * 释放REDIS锁
     * 
     * @param id
     */
    public void releaseRedisLock(T id) {
        String key = String.format(keyPattern, id);
        try {
            stringRedisTemplate.delete(key);
        } catch (Exception e) {
            logger.error("redis delete {} failed", key, e);
        }
    }

    /**
     * 等待锁
     * 
     * @param id
     * @param timeout
     */
    public boolean waitLocalLock(T id, int timeout) {
        long t1 = System.currentTimeMillis();
        Object o;
        while ((o = map.putIfAbsent(id, "")) != null) {
            safeSleep(1);
            if (System.currentTimeMillis() - t1 > timeout) {
                logger.warn("putIfAbsent {} timeout,result:{}", id, (o == null));
                break;
            }
        }
        if (o == null) {
            logger.info("putIfAbsent {} success", id);
            return true;
        } else {
            return false;
        }
    }

    public void releaseLocalLock(T id) {
        if (id == null) {
            return;
        }
        Object o = map.remove(id);
        logger.info("remove {} return {},size:{}", id, o, map.size());
    }

    private static void safeSleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
            Thread.currentThread().interrupt();
        }
    }
}
